#!/usr/bin/env python
#
#       culd.py
#
#       Copyright 2009 Robert Lischke <robert.lischke@gmail.com>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 3 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

from sys import argv                     # command-line arguments
from os import path                      # path handling
from getpass import getpass              # hide entered password
import urllib                            # handle login
import urllib2                           # handle urls
import cookielib                         # handle cookies



'''
TODO:
option: work with destination directory (not just the current one!)
option: resynchronize to citeulike by article-id (i will need a test account!)
option: instead of full download check for local .bib and local .pdf update to
newer version (or after asking)
option: use different formats! jabref, pybliographer, etc.
option:	articlefilename = articleid + '.pdf' (not readable, but short)
'''

def get_data_from_url(url):
	"""Get data located at an URL."""

	try:
		# TODO: we could add a timeout here
		response = opener.open(url)
	except IOError, e:
		if hasattr(e, 'reason'):
			print 'Failed to reach a server at "' + url + '"'
			print 'Reason: ', e.reason
#		elif hasattr(e, 'code'):
#			print 'The server couldn\'t fulfill the request.'
#			print 'Error code: ', e.code
		raise Exception()
	data = response.read()
	response.close()
	return data


def save_file(filename, data):
	"""Save data (e.g. from get_data_from_url() to a file."""

	# TODO: files are always overwritten!!!
	if len(data) > 1:                       # this is buggy! why did i do this?
		try:
			f = open(filename,'wb')
			f.write(data)
			f.close()
		except Exception:
			print 'Error: Writing file "' + filename + '"'
	else:
		print 'Error: Writing file "' + filename + '" (Empty file)'
	return


def citeulike_login(username, password):
	"""Login at CiteULike.

	Keyword arguments:
	username -- a CiteULike username
	password -- corresponding password

	"""

	_LOGIN_URL = 'http://www.citeulike.org/login.do'

	global opener                                  # global for cookies to work

	cj = cookielib.CookieJar()
	opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
	logindata = urllib.urlencode({'username' : username, \
	   'password' : password})
	try:
		opener.open(_LOGIN_URL, logindata)
	except IOError, e:
		if hasattr(e, 'reason'):
			print 'Failed to reach server at ' + _LOGIN_URL
			print 'Reason: ', e.reason
		elif hasattr(e, 'code'):
			print 'The server couldn\'t fulfill the request.'
			print 'Error code: ', e.code
	# login successful if cookie is set, len(cj) is 4 after login
	if len(cj) == 0:
		print 'Error: Invalid username or password'
		raise Exception()
	else:
		return


def get_bib(username, directory):
	"""Get CiteULike BibTeX file for username
	and store locally as "username.bib"

	Keyword arguments:
	username -- a (valid) CiteULike username

	"""

	_BIB_URL1 = 'http://www.citeulike.org/bibtex/user/'
	_BIB_URL2 = '?do_username_prefix=0&key_type=4' + \
	   '&incl_amazon=0&clean_urls=1&q=' # TODO: offer more options

	filename = username + '.bib'
	print 'Retrieving "' + filename + '" from citeulike.org'
	url = _BIB_URL1 + username + _BIB_URL2
	try:
		data = get_data_from_url(url)
	except Exception:
		print 'Error: Retrieving "' + filename + '" from citeulike.org'
		raise Exception()
	absolutefilename = path.join(directory, filename)
	save_file(absolutefilename, data)
	return absolutefilename


def parse_bib(absolutefilename):
	"""Parse local BibTeX file."""

	from pybtex.database.input import bibtex # bibtex parsing

	print 'Parsing "' + absolutefilename + '"'
	parser = bibtex.Parser()
	data = parser.parse_file(absolutefilename)
	return data


def get_article_id(bibkey):
	"""Return citulike-article-id stored in BibTeX entry."""
	try:
		id = bibdata.entries[bibkey].fields['citeulike-article-id']
	except Exception:
		# every entry should have an article-id, but you never know
		print 'Error: No citeulike-article-id for BibTeX entry "' + \
		   bibkey + '"'
		raise Exception()
	return id


def build_article_filename(bibkey):
	"""Return customized .pdf filename of BibTeX entry."""

	_FILETYPE = '.pdf'
	_MAX_LENGTH = 250

	def _validfilename(dirtyfilename):

		from string import join
		from string import ascii_letters
		from string import digits

		_VALID_CHARS = frozenset("-_.() %s%s" % (ascii_letters, digits))

		validfilename = ''.join(c for c in dirtyfilename if c in _VALID_CHARS)
		return validfilename

	try:
		author = bibdata.entries[bibkey].fields['author']
		# TODO: handle multiple authors (et.al)
	except Exception:
		try:
			editor = bibdata.entries[bibkey].fields['editor']
			author = editor + ' (Ed.)'
			# TODO: handle multiple editors (Eds.)
		except Exception:
			author = 'Unknown'
	try:
		year = bibdata.entries[bibkey].fields['year']
	except Exception:
		year = 'Unknown'
	try:
		title = bibdata.entries[bibkey].fields['title']
	except Exception:
		title = 'Unknown'
	filename = author + ' (' + year + ') ' + title
	filename = _validfilename(filename)
	# restrict to 255 characters
	filename = filename[0:_MAX_LENGTH] + _FILETYPE
	return filename


def add_entry_for_filename(bibkey, filename):
	"""Add BibTeX entry to bibdata referencing the filename. Currently
	only supports JabRef (new format!)."""

	_JABREF_DESCRIPTION = 'citeulikepdf:'
	_JABREF_FILETYPE = ':PDF'

	# TODO: the old JabRef format is "pdf = {filename}"
	def	_add_entry(_bibkey, _filename):
		temp_entry = bibdata.entries[_bibkey].fields
		temp_entry['file'] = _filename
		bibdata.entries[_bibkey].fields = temp_entry
		return

	filename_in_bib = ''
	new_filename_in_bib = _JABREF_DESCRIPTION + filename + _JABREF_FILETYPE

	try:
		filename_in_bib = bibdata.entries[bibkey].fields['file']
	except Exception:
		# add
		_add_entry(bibkey, new_filename_in_bib)
		return
	if filename_in_bib != new_filename_in_bib:
		# update
		_add_entry(bibkey, new_filename_in_bib)
		print 'Warning: Updated reference to "' + filename + '" in "' + \
			bibkey + '"'
	return


def write_bib(absolutefilename, data):
	"""Save bilbiography as filename."""

	from pybtex.database.output import bibtex as bibtex # bibtex export

	writer = bibtex.Writer(encoding='utf-8')
	writer.write_file(data, absolutefilename)


def usage():
	print 'culd - CiteULikeDownloader'
	print 'Usage: culd [-h] DIRECTORY'
	print 'Requires a valid directory name as argument.'


def main(argument):
	"""Putting it all together."""
	_PDFURL1 = 'http://www.citeulike.org/pdf/user/'
	_PDFURL2 = '/article/'

	global bibdata

	# process command-line argument(s)
	if (argument == ['']) or (argument == ['-h']) or \
		(argument == '--help') or (len(argument) == 0):
		usage()
		return 0

	outputdirectory = path.abspath(argument[:1][-1])
	if path.exists(outputdirectory):
		print 'culd - CiteULikeDownloader'
		print 'Using output directory "' + outputdirectory + '"'
	else:
		usage()
		return 0

	username = raw_input("CiteULike Username : ")
	password = getpass("CiteULike Password : ")

	try:
		citeulike_login(username, password)
	except Exception:
		return 1
	try:
		bibfilename = get_bib(username, outputdirectory)
	except Exception:
		return 1

	bibdata = parse_bib(bibfilename)
	bibkeys = bibdata.entries.keys()

	i = 0
	numberofbibkeys = len(bibkeys)
	for key in bibkeys:
		i = i + 1
		print 'Processing BibTeX key "' + key + '" (' + str(i) + '/' + \
		   str(numberofbibkeys) + ')'
		try:
			articleid = get_article_id(key)
		except Exception:
			pass
		else:
			articleurl = _PDFURL1 + username + _PDFURL2 + articleid
			articlefilename = build_article_filename(key)

			try:
				articledata = get_data_from_url(articleurl)
			except Exception:
				pass
			else:
				absolutearticlefilename = path.join(outputdirectory, \
				   articlefilename)
				save_file(absolutearticlefilename, articledata)
				# TODO: optional, use absolutefilename here
				add_entry_for_filename(key, articlefilename)

	filename_output = username + '-culd.bib'
	absolutefilename_output = path.join(outputdirectory, filename_output)
	write_bib(absolutefilename_output, bibdata)
	print 'Output written to "' + outputdirectory + '"'
	return 0


if __name__ == '__main__':
	main(argv[1:])